using System;
using System.Collections.Generic;
using Microsoft.Practices.EnterpriseLibrary.Data;
using System.Data;
using MSRI.HelperClasses;
using System.Data.Common;
using System.Globalization;
using Instedd.Labs.Geo.Library.Kml;
using Instedd.Labs.Geo.Library.GPS;


namespace Instedd.Labs.Geo.Library.Sms
{
    public class SmsMessagesManager
    {
        private Database db;

        private const string DeleteAllMessagesSQL = "DELETE FROM [Messages]";

        private const string SelectAllMessagesSQL = "SELECT [MessageId], [Time], [ReceivingNumber], [SenderNumber], [Lat], [Long], [Message], [AnsweredMessageId] FROM [Messages]";

        private const string SelectMessageByIdSQL = "SELECT [MessageId], [Time], [ReceivingNumber], [SenderNumber], [Lat], [Long], [Message], [AnsweredMessageId] FROM [Messages] WHERE [MessageId] = @msgId";

        private const string SelectMessagesBySenderNumberSQL = "SELECT [MessageId], [Time], [ReceivingNumber], [SenderNumber], [Lat], [Long], [Message], [AnsweredMessageId] FROM [Messages] WHERE [SenderNumber] = @senderNumber";

        private const string InsertMessageSQL = "INSERT INTO [Messages] ([MessageId], [Time], [ReceivingNumber], [SenderNumber], [Lat], [Long], [Message], [AnsweredMessageId]) VALUES (@messageId, @time, @receivingNumber, @senderNumber, @lat, @long, @message, @answeredMessageId)";

        const string SelectLatestMessageTimeBySenderNumberSQL = "SELECT MAX([Time]) as MaxTime FROM [Messages] WHERE [SenderNumber] = @senderNumber";
        const string SelectLatestMessageLocationBySenderNameSQL = "SELECT [Lat], [Long] FROM [Messages] WHERE [SenderNumber] = @senderNumber AND [Time] = @time AND [Lat] IS NOT NULL AND [Long] IS NOT NULL";

        ISenderNameLookupProvider senderNameLookupProvider;
        Configuration configuration;

        MessagePatternFormatter formatter;

        LatLong defaultLocation;

        IGeoCoder geoCoder;

        public SmsMessagesManager(Database db, ISenderNameLookupProvider senderNameLookupProvider, IGeoCoder geoCoder, Configuration configuration)
        {
            Guard.ArgumentNotNull(db, "db");
            Guard.ArgumentNotNull(configuration, "configuration");
            Guard.ArgumentNotNull(senderNameLookupProvider, "senderNameLookupProvider");

            this.db = db;
            this.configuration = configuration;
            this.senderNameLookupProvider = senderNameLookupProvider;
            this.geoCoder = geoCoder;
            this.formatter = new MessagePatternFormatter();

            LatLong.TryParse(configuration.DefaultLocation, out defaultLocation);
        }

        public IEnumerable<SmsExtension> GetAllMessages()
        {
            List<SmsExtension> result = new List<SmsExtension>();

            using (IDataReader reader = this.db.ExecuteReader(CommandType.Text, SelectAllMessagesSQL))
            {
                while (reader.Read())
                {
                    result.Add(GetSmsExtension(reader));
                }
            }

            return result;
        }

        public void DeleteAllMessages()
        {
            using (DbCommand command = db.GetSqlStringCommand(DeleteAllMessagesSQL))
            {
                db.ExecuteNonQuery(command);
            }
        }

        public SmsExtension GetMessage(Predicate<SmsExtension> predicate)
        {
            return new List<SmsExtension>(GetAllMessages()).Find(predicate);
        }

        public SmsExtension GetMessageById(string messageId)
        {
            using (DbCommand command = db.GetSqlStringCommand(SelectMessageByIdSQL))
            {
                db.AddInParameter(command, "msgId", DbType.String, messageId);
                using (IDataReader reader = db.ExecuteReader(command))
                {
                    if (reader.Read())
                    {
                        return GetSmsExtension(reader);
                    }
                }
            }

            return null;
        }

        public IEnumerable<SmsExtension> GetMessagesBySenderNumber(string senderNumber)
        {
            List<SmsExtension> messages = new List<SmsExtension>();

            using (DbCommand command = db.GetSqlStringCommand(SelectMessagesBySenderNumberSQL))
            {
                db.AddInParameter(command, "senderNumber", DbType.String, senderNumber);
                using (IDataReader reader = db.ExecuteReader(command))
                {
                    while (reader.Read())
                    {
                        messages.Add(GetSmsExtension(reader));
                    }
                }
            }

            return messages;
        }

        // TODO: This method should not be static
        public static Dictionary<string, Dictionary<LatLong, List<SmsExtension>>> GetMessageGroupBySenderNameAndLocation(IEnumerable<SmsExtension> messages)
        {
            Dictionary<string, Dictionary<LatLong, List<SmsExtension>>> result = new Dictionary<string, Dictionary<LatLong, List<SmsExtension>>>();

            foreach (SmsExtension message in messages)
            {
                SmsExtension messageToEvaluate = message.IsReplay ? message.AnsweredMessage : message;

                if (!result.ContainsKey(messageToEvaluate.SenderName))
                {
                    result.Add(messageToEvaluate.SenderName, new Dictionary<LatLong, List<SmsExtension>>());
                }
                if (!result[messageToEvaluate.SenderName].ContainsKey(messageToEvaluate.Location))
                {
                    result[messageToEvaluate.SenderName].Add(messageToEvaluate.Location, new List<SmsExtension>());
                }

                result[messageToEvaluate.SenderName][messageToEvaluate.Location].Add(message);
            }

            return result;
        }

        public void Save(SmsExtension sms)
        {
            

            using (DbCommand command = db.GetSqlStringCommand(InsertMessageSQL))
            {
                db.AddInParameter(command, "messageId", DbType.String, sms.GetMessageIdAsString());
                if (sms.AnsweredMessage != null)
                {
                    db.AddInParameter(command, "answeredMessageId", DbType.String,
                        sms.AnsweredMessage.GetMessageIdAsString());
                }
                else
                {
                    db.AddInParameter(command, "answeredMessageId", DbType.String, DBNull.Value);
                }
                db.AddInParameter(command, "time", DbType.DateTime, sms.Sms.Time);
                db.AddInParameter(command, "receivingNumber", DbType.String, sms.ReceivingNumber);
                if (sms.IsReplay)
                {
                    db.AddInParameter(command, "senderNumber", DbType.String, this.configuration.LocalNumber);
                }
                else
                {
                    db.AddInParameter(command, "senderNumber", DbType.String, sms.Sms.Number);
                }

                if (sms.Location != null)
                {
                    db.AddInParameter(command, "lat", DbType.Double, sms.Location.Lat);
                    db.AddInParameter(command, "long", DbType.Double, sms.Location.Long);
                }
                else
                {
                    db.AddInParameter(command, "lat", DbType.Double, DBNull.Value);
                    db.AddInParameter(command, "long", DbType.Double, DBNull.Value);
                }
                db.AddInParameter(command, "message", DbType.String, sms.Message);

                db.ExecuteNonQuery(command);
            }
        }

        private SmsExtension GetSmsExtension(IDataRecord record)
        {
            SmsClass sms = new SmsClass();
            sms.Time = (DateTime)record["Time"];
            sms.MessageID = new Guid((string)record["MessageId"]);

            LatLong location = null;
            if (record["Lat"] != DBNull.Value && record["Long"] != DBNull.Value)
            {
                location = new LatLong((double)record["Lat"], (double)record["Long"]);
            }

            SmsExtension message = new SmsExtension(
                sms,
                location,
                (string)record["Message"],
                this.senderNameLookupProvider.LookupSenderName((string)record["SenderNumber"]),
                (string)record["ReceivingNumber"]
                );

            string answeredMessageId = record["AnsweredMessageId"] as string;
            bool isReplay = answeredMessageId != null;
            if (isReplay)
            {
                sms.Number = message.ReceivingNumber;
                message.AnsweredMessage = GetMessageById(answeredMessageId);
            }
            else
            {
                sms.Number = (string)record["senderNumber"];
            }

            return message;
        }

        public SmsExtension Create(SmsClass sms)
        {
            try
            {
                LatLong latLong = null;
                string message = null;

                string[] messageParts = this.formatter.SplitMessage(this.formatter.Format(sms.Body));

                if (messageParts.Length == 3)
                {
                    latLong = LatLong.Parse(messageParts[0], messageParts[1]);
                    message = messageParts[2];

                    // forgetting minus on longitude correcting to negative. 
                    if (latLong.Long > 0d)
                    {
                        latLong.Long = latLong.Long * -1;
                    }
                }
                else if (messageParts.Length == 2)
                {
                    message = messageParts[1];
                    latLong = GetMessageLocationUsingPostalAddress(sms.Number, messageParts[0], messageParts[1]);
                }
                else if (messageParts.Length == 1)
                {
                    message = messageParts[0];
                    latLong = GetMessageLocationUsingLatWellKnownLocation(sms.Number, message);
                }

                return new SmsExtension(sms,
                    latLong,
                    message,
                    this.senderNameLookupProvider.LookupSenderName(sms.Number),
                    this.configuration.LocalNumber
                    );
            }
            catch (InvalidBodyFormatException)
            {
                throw;
            }
            catch (Exception ex)
            {
                // Move to resources
                throw new InvalidBodyFormatException(string.Format("The SMS content '{0}' could not be parsed.", sms.Body), ex);
            }
        }


        private LatLong GetMessageLocationUsingLatWellKnownLocation(string senderNumber, string message)
        {
            LatLong latLong = GetLatestLocation(senderNumber);

            if (latLong == null)
            {
                if (defaultLocation != null)
                {
                    latLong = defaultLocation;
                }
                else
                {
                    throw new InvalidBodyFormatException(string.Format("The message '{0}' doesn't contain the location. The latest well known location could not be determined and the default location was not configured.", message));
                }
            }

            return latLong;
        }

        private LatLong GetMessageLocationUsingPostalAddress(string senderNumber, string postalAddress, string message)
        {
            LatLong latLong = null;

            if (geoCoder != null)
            {
                latLong = this.geoCoder.GetLatLongFromPostalAddress(postalAddress);
            }

            if (latLong == null)
            {
                try
                {
                    latLong = GetMessageLocationUsingLatWellKnownLocation(senderNumber, message);
                }
                catch (InvalidBodyFormatException ex)
                {
                    throw new InvalidBodyFormatException(string.Format("The postal address '{0}' of the message '{1}' could not be determined", postalAddress, message), ex);
                }  
            }

            return latLong;
        }

        private LatLong GetLatestLocation(string senderNumber)
        {
            DateTime? lastMessageTime = GetLatestMessageTime(senderNumber);

            if (lastMessageTime != null)
            {
                using (DbCommand command = db.GetSqlStringCommand(SelectLatestMessageLocationBySenderNameSQL))
                {
                    db.AddInParameter(command, "senderNumber", DbType.String, senderNumber);
                    db.AddInParameter(command, "time", DbType.DateTime, lastMessageTime);
                    using (IDataReader reader = db.ExecuteReader(command))
                    {
                        if (reader.Read())
                        {
                            return new LatLong((double)reader["Lat"], (double)reader["Long"]);
                        }
                    }
                }
            }

            return null;
        }

        private DateTime? GetLatestMessageTime(string senderNumber)
        {
            using (DbCommand command = db.GetSqlStringCommand(SelectLatestMessageTimeBySenderNumberSQL))
            {
                db.AddInParameter(command, "senderNumber", DbType.String, senderNumber);
                using (IDataReader reader = db.ExecuteReader(command))
                {
                    if (reader.Read() && reader["MaxTime"] != DBNull.Value)
                    {
                        return (DateTime)reader["MaxTime"];
                    }
                }
            }

            return null;
        }
    }
}